#!/usr/bin/env python3.9
# This generates 'analogue' SSTV from a (preferable very high resolution)
# image. The output is just in greyscale, because, well, it looks awesome.
# It's slow, and Python uses up a ridiculous amount of memory, but it's
# just accessible code for playing with.
# It's just PD modes for this, as those give 50% to the lumanence.
# --
# Licenced according to the GNU Public Licence version 3.0
# Chris Baird,, June 2022 <vk2cjb@gmail.com>

import math, struct, sys
from PIL import Image

specs = [
    # name   VIS scanlen fudge     modesize
    ( "PD50", 93,  4394, 18632.85, (320,256)),
    ( "PD90", 99,  8173, 33748.00, (320,256)),
    ("PD120", 95,  5838, 24408.65, (640,496)),
    ("PD160", 98,  9389, 38614.60, (512,400)),
    ("PD180", 96,  8787, 36206.05, (640,496)),
    ("PD240", 97, 11736, 48003.25, (640,496)), # decodes poorly in QSSTV
    ("PD290", 94, 10984, 44992.50, (800,616))]
modelist = [x[0] for x in specs]
sample_rate, ph, nframes, mode, outfile = 48000, 0.0, 0, 4, ""
showident, leftident, redident, pattern = True, False, False, 1
audio = bytearray()

###

while len(sys.argv) > 1:
    sys.argv.pop(0)
    s = sys.argv[0]
    if s == "-r": redident = True
    elif s == "-l": leftident = True
    elif s == "-p":
        pattern = int(sys.argv[1])
        sys.argv.pop(0)
    elif s == "-o":
        outfile = sys.argv[1]
        sys.argv.pop(0)
    elif s in modelist: mode = modelist.index(s)
    else:
        print ("You needed instructions? oh.")
        sys.exit(1)

(name, VIS, scanlen, fudge, modesize) = specs[mode]
height = modesize[1]

###

def sampleout(freq):
    global ph,nframes,audio
    s = int(math.sin(ph / sample_rate) * 3276.8) # constant can vary
    if s < 0: s += 65536
    audio += bytes([s & 255,s >> 8])
    ph += math.tau * freq
    nframes += 1


def tonegen(freq,samples):
    for _ in range(samples): sampleout(freq)

###

def vis(c):
    tonegen(1900,14400)  # leader - 1900 Hz, 300 ms/570 cycles/14400
    tonegen(1200,480)    # break - 1200 Hz,  10 ms/12 cycles/480
    tonegen(1900,14400)  # leader again
    tonegen(1200,1440)   # startbit - 1200 Hz, 30 ms/36 cycles

    p = 0
    for i in (1,2,4,8,16,32,64,128):
        if i == 128 and (p & 1): c |= 128
        if (c & i):
            p += 1
            tonegen(1100,1440)
        else:
            tonegen(1300,1440)
    tonegen(1200,1440)          # stop bit - 30 ms

###

id = (0,0,0,0,1,0,1,0,0,1,0,1,0,0,1,1,1,0,0,1,1,1,0,0,0,0,1,0,0,1,1,1,0,0,0,0,
      0,0,0,0,1,0,1,0,0,1,0,1,0,0,0,0,1,0,0,1,0,0,0,0,0,0,1,0,0,1,0,1,0,0,0,0,
      0,0,0,0,1,0,1,0,0,1,1,0,0,0,1,1,1,0,0,1,0,0,0,0,0,0,1,0,0,1,1,1,0,0,0,0,
      0,0,0,0,1,0,1,0,0,1,0,1,0,0,1,0,0,0,0,1,0,0,0,0,1,0,1,0,0,1,0,1,0,0,0,0,
      0,0,0,0,0,1,0,0,0,1,0,1,0,0,1,1,1,0,0,1,1,1,0,0,1,1,1,0,0,1,1,1,0,0,0,0)

def doidentline(y,side):
    yt = 36 * (int((60*y / modesize[1])) % 10)
    for x in range(scanlen):
        xx = int((72*x / scanlen)) % 36
        i = xx + yt
        freq = 1900
        if i < len(id) and id[i]: freq = 2300
        if not side and x < (scanlen/2): freq = 1900
        if side and x > (scanlen/2): freq = 1900
        sampleout(freq)

###

nlines = modesize[0] // 16
xaspect = scanlen / modesize[0]

strings = []
for i in range(nlines):
    p = (i*scanlen) / (nlines-1)
    if pattern == 1:
        strings.append((p, 0, scanlen/2, height))

    if pattern == 2:
        #strings.append((p, height*0.33, scanlen/2, height*0.66))
        strings.append((p, 0, scanlen-p, height))

    if pattern == 3:
        def cpos(a):
            ph = math.tau / nlines
            # Remember - in FP, divisions are Evil
            x = (scanlen / 2) + ((math.sin(ph*a) * scanlen * modesize[1]) \
                                 / (2*modesize[0]))
            y = (height / 2)*(1 + math.cos(ph*a))
            return (x, y)

        if i > 0:
            x1, y1 = cpos(i)
            for j in range(i):
                x2, y2 = cpos(j)
                strings.append((x1, y1, x2, y2))

#

def intersectline (l1,l2):
    x1,y1,x2,y2 = l1
    x3,y3,x4,y4 = l2

    if (y3 < min(y1,y2)) or (y3 > max(y1,y2)): return (-1,-1)

    def xfix(x): return (x * modesize[0]) / scanlen

    x1, x2, x3, x4 = xfix(x1), xfix(x2), xfix(x3), xfix(x4)

    d = (x1-x2)*(y3-y4) + (y2-y1)*(x3-x4)
    if d == 0: return (-1,-1)

    p1, p2 = x1*y2 - y1*x2, x3*y4 - y3*x4
    px, py = p1*(x3-x4) + p2*(x2-x1), p1*(y3-y4) + p2*(y2-y1)

    return (xaspect * px / d, py / d)


# It's not one of those times to just brute-force things...
scansort = {}
for l in range(len(strings)):
    for y in range(height):
        if not y in scansort: scansort[y] = ()
        ix, iy = intersectline(strings[l], (0,y, scanlen,y))
        if iy != -1: scansort[y] += (strings[l],)

###

def doresline(y):
    for x in range(scanlen):
        bestd = 1000000.0

        for string in scansort[y]:
            ix, iy = intersectline(string, (0,y, scanlen,y))
            if iy != -1:
                d = abs(x - ix) / xaspect
                if d < bestd: bestd = d

        freq = 1500 + 1600*bestd
        if freq > 2300: freq = 2300

        sampleout(freq)

### Main

tonegen(0,24000)
vis(VIS)

identstart, f, t = height * 0.80, 0, 0

for y in range(0,height,2):
    print (y, end='\r')
    f = int(y*fudge*0.5 - t)
    onf = nframes
    tonegen(1200,960+f)
    tonegen(1500,96)
    doresline(y)

    if showident:
        if redident:
            if y > identstart: doidentline(y,leftident)
            else: tonegen(1900,scanlen)
            tonegen(1900,scanlen)
        else:
            tonegen(1900,scanlen)
            if y > identstart: doidentline(y,leftident)
            else: tonegen(1900,scanlen)
    else:
        tonegen(1900,scanlen)
        tonegen(1900,scanlen)
    doresline(y+1)

    t += nframes - onf

tonegen(0,24000)

###

if not outfile: outfile = "restest-" + name + "-" + str(pattern) + ".wav"

wav = open(outfile,"wb")

s = struct.Struct(b"<ccccLccccccccLHHLLHHccccL")
wav.write(s.pack(b'R',b'I',b'F',b'F', # riff_header
                 2*len(audio)+44,     # wav_size
                 b'W',b'A',b'V',b'E', # wave_header
                 b'f',b'm',b't',b' ', # fmt_header
                 16,                  # fmt_chunk_size
                 1,                   # audio_format = PCM
                 1,                   # num_channels
                 sample_rate,
                 sample_rate*1*2,     # byte_rate
                 2,                   # sample_alignment
                 16,                  # bit_depth
                 b'd',b'a',b't',b'a',
                 2*len(audio)))       # data_bytes
wav.write(audio)                      # thanks to bytearray
wav.close()

###
